Elements Revisited
Video Summary
A few years ago, someone shared a sandbox with me that fundamentally changed my understanding of React.
The code looks like this:
import React from 'react';
function App() { const counterElem = <Counter />;
return ( <div> {counterElem} {counterElem} </div> );}
function Counter() { const [count, setCount] = React.useState(0);
return ( <button onClick={() => setCount(count + 1)}> {count} </button> );}
export default App;
That Counter
component is pretty typical: it has a count
state variable that starts at 0. The value is displayed within a button, and the button can be clicked to increment the value.
We're doing something sorta funky in the parent component though:
const counterElem = <Counter />;
return ( <div> {counterElem} {counterElem} </div>);
We're storing the counterElem
in a variable, and then duplicating that variable within the JSX.
In the UI, we see two counters, side by side:
Here's the question: What do you think will happen when we click the first button?
Will both buttons increase from 0 to 1, staying in lockstep with each other? Or will each button have its own count
state, which can be incremented individually?
Below, you'll find this code in a playground. Test your intuition by seeing what happens!
If you're surprised by the result, spend a few minutes seeing if you can understand what's going on. We'll discuss below.
Code Playground
Let's discuss.
Video Summary
I'll warn you right now: this is a dense video, and I won't be able to summarize it super well. If you normally read the summaries instead of watching the videos, you might wanna make an exception for this one!
As you've likely discovered, each counter is independent. This surprised me! I expected them to share the same state.
The reason I thought they would share the same state is because we only have a single React element.
Let's look at this without the JSX:
const counterElem = React.createElement(Counter);// -> { type: Counter, props: {} }
The createElement
method creates a React element, a plain JS object. We're then storing a reference to this object in a variable, counterElem
, and referencing that variable twice in the returned code.
And so, if we only have a single React element, won't that mean we only wind up with a single React component instance??
The thing to understand is that “creating an element” and “rendering a component” are two entirely different things.
Let's zoom out a bit. In the index.js
file, we have this code:
import App from "./App";
const container = document.getElementById('root');const root = createRoot(container);
root.render(<App />);
We create an App
element ({ type: App }
), and pass that element to React. We're asking React to render this element for us.
I've been saying throughout this course that React elements are descriptions of something we want to create. It's a bit like an architect's plans.
I recently learned that architect plans come in several levels of details:
The element we've created, { type: App }
is like level 0. It doesn't tell us anything about the actual UI we want React to render!
To get more granular, React has to render the App
component. The App
function returns a React element, a more granular picture of the application. Here's what that looks like:
const counterElem = { type: Counter, props: {} };
const appSketch = { type: 'div', props: {}, children: [ counterElem, counterElem ],};
This is maybe a Level 2 sketch; we know that we need a div with 2 children, but we don't know what those children are, exactly!
React will then map over the children
and recursively follow the same plan: we render the Counter
component to fill in the details:
const appSketch = { type: 'div', props: {}, children: [ { type: 'button', props: { onClick: function () {}, }, children: 0, }, { type: 'button', props: { onClick: function () {}, }, children: 0, }, ],};
The fact that each counterElem
is a reference to the same object doesn't really matter. React never considers this! Instead, this singular description is used multiple times, to render multiple components.
We started at the top, with <App />
, and we've drilled down through the app. Whenever a React element was referencing a component, we've rendered that component, filling in the complete picture. Our sketch is complete!
The cool thing here is that we have an exact description of the actual UI. If we inspect it in the browser, it looks like this:
<div> <button> 0 </button> <button> 0 </button></div>
Notice how our JS object describes this markup perfectly?
In the past, the React team referred to this as the Virtual DOM. Each component produces a chunk of JS that describes a slice of the application. By rendering components when we run into them, we can put all of those slices together and get a detailed sketch of the UI we want.
(I think the React team stopped using this term because it was confusing. They're not wrong; we're in friggin Module 5 of this course, and we're just now reaching the point where we have enough context to make sense of this!)
After doing all of this work, React “commits” these changes by creating the necessary DOM nodes.
Let's look at one other example. This one is sort of the opposite, but it plays on the same foundational ideas.
Here's the code:
import React from 'react';
function App() { const [color, setColor] = React.useState(null);
return ( <div> {color ? ( <Counter color={color} /> ) : ( <Counter /> )}
<label htmlFor="color-picker"> Select color: <input id="color-picker" type="color" value={color} onChange={(event) => { setColor(event.target.value); }} /> </label> </div> );}
function Counter({ color }) { const [count, setCount] = React.useState(0);
return ( <button style={{ color }} onClick={() => setCount(count + 1)} > {count} </button> );}
export default App;
Our Counter
component now has a color
prop, and we have a controlled color input that changes that value.
We're doing something a bit curious here though:
{color ? ( <Counter color={color} />) : ( <Counter />)}
On the first render, color
is null
, and so the falsy branch is taken; we render a <Counter />
. When the user selects a new color, color
is a string like #123123
, and so the truthy branch is chosen instead.
Here's the question: When we flip from the falsy branch to the truthy branch, will we destroy/recreate the Counter
component instance? Or will it reuse the existing instance?
My original intuition was that it would destroy and recreate the instance. After all, it's a different chunk of JSX!
But React doesn't have any visibility into how our code is structured. It only sees what we return.
Let's sketch it out. On the first render, we return a React element that looks like this:
const firstSketch = { type: 'div', props: {}, children: [ { type: Counter, props: {} }, // ✂️ Ignoring the `<label>` since it doesn't change ]}
Then, suppose the user changes the color. The new sketch looks like this:
const secondSketch = { type: 'div', props: {}, children: [ { type: Counter, props: { color: '#123123', } }, // ✂️ Ignoring the `<label>` since it doesn't change ]}
The only difference, as far as React can tell, is that we've added a new color
prop. And props change all the time; we don't typically destroy/recreate a component instance when a prop changes!
This is another one of those counterintuitive situations. Our brains say "but it's different! It's a different chunk of JSX!". But as we've been learning, React elements are descriptions of what we want. The identity of that description doesn't matter.
This stuff is hard to wrap our minds around. I've been using React for a long time and it still trips me up! This might be one of those lessons that is worth revisiting in a few months, to solidify these ideas.
This idea is covered in even more depth in the official React documentation:
Here's the second sandbox from the video, the one with a color control:
Code Playground
- Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components. at input at label at div at App (https://sandpack-bundler.vercel.app/App.js:23:42)